package org.xlp.utils.net;

import org.xlp.assertion.AssertUtils;
import org.xlp.json.Json;
import org.xlp.utils.*;
import org.xlp.utils.io.XLPIOUtil;

import javax.net.ssl.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Description: http或https请求客户端
 * <br/>date: 2025/2/25
 *
 * @version 1.0
 * @author: xlp
 */
public class HttpRequestClient implements Closeable {
    /**
     * url连接对象
     */
    private HttpURLConnection connection;

    /**
     * 以文本格式存储响应数据
     */
    private String responseData;

    /**
     * 以流模式存储响应数据
     */
    private InputStream inputStream;

    private HttpRequestClient(HttpURLConnection connection){
        this.connection = connection;
    }

    /**
     * 关闭资源
     *
     * @throws IOException if an I/O error occurs
     */
    @Override
    public void close() throws IOException {
        responseData = null;
        XLPIOUtil.closeInputStream(inputStream);
        inputStream = null;

        if (connection != null) connection.disconnect();
    }

    /**
     * 获取响应流
     * @return
     * @throws IOException
     */
    public InputStream getInputStream() throws IOException {
        if (inputStream != null) return inputStream;

        int code = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == code) {
            inputStream = connection.getInputStream();
        } else {
            throw new IOException("响应码不为200，为[" + code + "],获取响应数据失败！");
        }
        return inputStream;
    }

    /**
     * 获取文本响应数据
     *
     * @param charsetName 字符编码
     * @return
     */
    public String getResponseData(String charsetName) throws IOException {
        if (responseData != null) return responseData;
        if (inputStream == null){
            getInputStream();
        }
        charsetName = XLPStringUtil.isEmpty(charsetName) ? getResponseCharsetName() : charsetName;
        return XLPIOUtil.toString(inputStream, false, charsetName);
    }

    /**
     * 获取文本响应数据
     *
     * @return
     */
    public String getResponseData() throws IOException {
        return getResponseData(null);
    }

    /**
     * 获取响应字符编码名称
     * @return
     */
    private String getResponseCharsetName() {
        // 获取输入流,获取响应结果
        Map<String, List<String>> map = connection.getHeaderFields();
        //获取响应
        List<String> headerList = map.get("Content-Type");
        if (headerList != null && headerList.size() > 0) {
            for (String header : headerList) {
                if (header != null && header.indexOf("charset") > 0) {
                    return header.split("=")[1];
                }
            }
        }
        // 如果没有获取到响应头编码,则默认用 utf-8
        return XLPCharsetUtil.UTF8;
    }

    /**
     * HttpRequestClient构建器
     */
    public static class HttpRequestClientBuilder {
        public static final String FORM_URLENCODED = "application/x-www-form-urlencoded";
        public static final String JSON_TYPE = "application/json";
        public static final String MULTIPART_FORM_DATA = "multipart/form-data";

        public static final String BOUNDARY = "----WebKitFormBoundary";

        public final static String POST = "POST";
        // get请求
        public final static String GET = "GET";

        public final static String PUT = "PUT";

        public final static String DELETE = "DELETE";

        public static final String CONTENT_TYPE_KEY = "Content-Type";

        /**
         * 标记是否使用缓存
         */
        private boolean useCaches;

        /**
         * 标记是否是ssl安全连接
         */
        private boolean ssl;

        private X509TrustManager manager;

        /**
         * 连接超时时间（单位：毫秒） 默认5秒
         */
        private int connectTimeout = 5000;

        /**
         * 响应超时时间（单位：毫秒） 默认5秒
         */
        private int readTimeout = 5000;

        /**
         * 请求方式，默认GET请求
         */
        private String requestMethod = "GET";

        private final static String DEFAULT_CONTENT_TYPE = FORM_URLENCODED + ";charset=utf-8";

        /**
         * 请求内容格式类型
         */
        private String contentType = DEFAULT_CONTENT_TYPE;

        /**
         * 请求地址
         */
        private String url;

        /**
         * <code>{@link URLEncoder}</code>编码方式
         */
        private String encodeCharset = XLPCharsetUtil.UTF8;

        /**
         * 请求头设置
         */
        private Map<String, Object> headers = new HashMap<>();

        /**
         * 请求数据
         */
        private Object data;

        /**
         * 请求参数
         */
        private Map<String, Object> params = new HashMap<>();

        /**
         * 请求数据格式化器
         */
        private RequestDataFormatter formatter;

        private HttpRequestClientBuilder(){}

        public HttpRequestClientBuilder useCaches(boolean useCaches){
            this.useCaches = useCaches;
            return this;
        }

        public HttpRequestClientBuilder ssl(boolean ssl){
            return ssl(ssl, null);
        }

        public HttpRequestClientBuilder ssl(boolean ssl, X509TrustManager manager){
            this.ssl = ssl;
            this.manager = manager;
            return this;
        }

        /**
         * 设置连接超时时间（单位：毫秒）
         * @param connectTimeout
         * @return
         */
        public HttpRequestClientBuilder connectTimeout(int connectTimeout){
            this.connectTimeout = connectTimeout;
            return this;
        }

        /**
         * 设置去取超时时间（单位：毫秒）
         * @param readTimeout
         * @return
         */
        public HttpRequestClientBuilder readTimeout(int readTimeout){
            this.readTimeout = readTimeout;
            return this;
        }

        public HttpRequestClientBuilder contentType(String contentType){
            this.contentType = contentType;
            return this;
        }

        /**
         * 设置请求地址
         * @param url
         * @return
         * @throws NullPointerException 假如请求地址不能为null或空，则抛出该异常
         */
        public HttpRequestClientBuilder url(String url){
            AssertUtils.isNotNull(url, "请求地址不能为null或空！");
            this.url = url;
            return this;
        }

        public HttpRequestClientBuilder encodeCharset(String encodeCharset){
            if (!XLPStringUtil.isEmpty(encodeCharset)) {
                this.encodeCharset = encodeCharset;
            }
            return this;
        }

        public HttpRequestClientBuilder post(){
            this.requestMethod = POST;
            return this;
        }

        public HttpRequestClientBuilder get(){
            this.requestMethod = GET;
            return this;
        }

        public HttpRequestClientBuilder put(){
            this.requestMethod = PUT;
            return this;
        }

        public HttpRequestClientBuilder delete(){
            this.requestMethod = DELETE;
            return this;
        }

        public HttpRequestClientBuilder header(String key, Object value){
            headers.put(key, value);
            if (CONTENT_TYPE_KEY.equalsIgnoreCase(key)){
                contentType = (String) value;
            }
            return this;
        }

        public HttpRequestClientBuilder headers(Map<String, Object> headers){
            if (headers != null){
                this.headers.putAll(headers);
                if (headers.containsKey(CONTENT_TYPE_KEY)){
                    contentType = (String) headers.get(CONTENT_TYPE_KEY);
                }
            }
            return this;
        }

        public HttpRequestClientBuilder param(String key, Object value){
            params.put(key, value);
            return this;
        }

        public HttpRequestClientBuilder params(Map<String, Object> params){
            if (params != null){
                this.params.putAll(params);
            }
            return this;
        }

        /**
         * 设置请求数据
         * @param data
         * @return
         */
        public HttpRequestClientBuilder data(Object data){
            this.data = data;
            return this;
        }

        /**
         * 设置请求数据格式化器
         * @param formatter
         * @return
         */
        public HttpRequestClientBuilder requestDataFormat(RequestDataFormatter formatter){
            this.formatter = formatter;
            return this;
        }

        /**
         * 构建请求客户端对象
         * @return
         */
        public HttpRequestClient builder() throws Exception {
            String _url = this.url;
            //拼接请求参数
            if (_url != null && !params.isEmpty()){
                if (!_url.contains("?")) _url += "?";
                _url += serializeParams(params);
            }

            URL url = new URL(_url);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            HttpRequestClient client = new HttpRequestClient(connection);
            if (ssl){
                X509TrustManager manager = this.manager == null ? new XLPX509TrustManager() : this.manager;
                // 创建SSLContext对象，并使用我们指定的信任管理器初始化
                TrustManager[] tm = { manager };
                SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
                sslContext.init(null, tm, new java.security.SecureRandom());
                // 从上述SSLContext对象中得到SSLSocketFactory对象
                SSLSocketFactory ssf = sslContext.getSocketFactory();
                ((HttpsURLConnection) connection).setSSLSocketFactory(ssf);
            }

            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(useCaches);
            //设置超时时间
            //setConnectTimeout：设置连接主机超时（单位：毫秒）
            //setReadTimeout：设置从主机读取数据超时（单位：毫秒）
            connection.setConnectTimeout(connectTimeout);
            connection.setReadTimeout(readTimeout);
            //设置请求方法
            connection.setRequestMethod(requestMethod);

            String boundary = null;
            if (isaMultipartForm()){
                boundary = BOUNDARY + System.currentTimeMillis();
                contentType = MULTIPART_FORM_DATA + "; boundary=" + boundary;
            }

            //设置连接请求头信息
            setConnectionHeaders(connection);
            //建立连接
            connection.connect();

            if ((POST.equals(requestMethod) || PUT.equals(requestMethod)) && data != null){
                //设置请求数据
                byte[] requestData = null;
                if (formatter != null){
                    requestData = formatter.requestDataFormat(data, contentType);
                } else if (isaMultipartForm()){
                    //处理带文件的数据，暂时只支持data为map的数据，其它类型不处理
                    requestMultipartData(connection, boundary);
                } else {
                    requestData = getNormalData();
                }
                if (requestData != null){
                    try (OutputStream outputStream = connection.getOutputStream()){
                        outputStream.write(requestData);
                        outputStream.flush();
                    }
                }
            }
            return client;
        }

        /**
         * 判断是否是multipart请求
         * @return 返回true：是，false：不是
         */
        private boolean isaMultipartForm() {
            return contentType != null && contentType.toLowerCase(Locale.US).startsWith(MULTIPART_FORM_DATA);
        }

        /**
         * 暂时只处理{@link HttpRequestClientBuilder#data}为{@link Map}的数据，其它不处理
         * @param connection
         * @param boundary
         */
        private void requestMultipartData(HttpURLConnection connection, String boundary) throws IOException {
           if (data instanceof Map){
               Map<?, ?> map = (Map<?, ?>) data;
               if (!map.isEmpty()) {
                   try (OutputStream outputStream = connection.getOutputStream();
                        PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, encodeCharset),
                                true)) {
                       for (Map.Entry<?, ?> entry : map.entrySet()) {
                           if (entry.getKey() == null) continue;
                           String key = String.valueOf(entry.getKey());
                           Object v = entry.getValue();
                           if (v instanceof InputStream) {
                               // 2. 写入文件字段
                               writer.append("--").append(boundary).append("\r\n");
                               writer.append("Content-Disposition: form-data; name=\"")
                                       .append(encode(key)).append("\"; filename=\"")
                                       .append(encode(key)) // 编码文件名
                                       .append("\"\r\n");
                               writer.append("Content-Type: application/octet-stream\r\n\r\n");
                               writer.flush();
                               try(InputStream input = (InputStream) v) {
                                   XLPIOUtil.copy(input, outputStream, false);
                                   outputStream.flush();
                               }
                           } else if (v instanceof File) {
                               File file = (File) v;
                               // 2. 写入文件字段
                               writer.append("--").append(boundary).append("\r\n");
                               writer.append("Content-Disposition: form-data; name=\"")
                                       .append(encode(key)).append("\"; filename=\"")
                                       .append(encode(file.getName())) // 编码文件名
                                       .append("\"\r\n");
                               writer.append("Content-Type: application/octet-stream\r\n\r\n");
                               writer.flush();
                               try(InputStream input = new FileInputStream(file)) {
                                   XLPIOUtil.copy(input, outputStream, false);
                                   outputStream.flush();
                               }
                           } else {
                               // 1. 写入文本字段
                               writer.append("--").append(boundary).append("\r\n");
                               writer.append("Content-Disposition: form-data; name=\"")
                                       .append(encode(key)).append("\"\r\n\r\n");
                               writer.append(encode(String.valueOf(v))).append("\r\n");
                               writer.flush();
                           }
                       }

                       // 4. 结束边界
                       writer.append("\r\n--").append(boundary).append("--\r\n");
                       writer.flush();
                   }
               }
           }
        }

        private byte[] getNormalData() throws UnsupportedEncodingException {
            String textData;
            if (contentType != null && contentType.toLowerCase(Locale.US).startsWith(FORM_URLENCODED)){
                if (data instanceof Map){
                    textData = serializeParams((Map<?, ?>) data);
                } else {
                    textData = String.valueOf(data);
                }
                return textData.getBytes(encodeCharset);
            }
            Class<?> cs = data.getClass();
            // 判断返回值是否是数字类型，或基础类型，或包装类型，或字符串类型
            if (XLPPackingTypeUtil.isNumber(data)
                    || XLPPackingTypeUtil.isOtherRawOrPackingType(cs)
                    || data instanceof CharSequence
                    || data instanceof Json){
                textData = data.toString();
            } else {
                textData = Json.toJsonString(data);
            }
            return textData.getBytes(encodeCharset);
        }

        private void setConnectionHeaders(HttpURLConnection connection) throws IOException{
            for (Map.Entry<String, Object> entry : headers.entrySet()) {
                String key = entry.getKey();
                Object v = entry.getValue();
                connection.setRequestProperty(encode(key), encode(String.valueOf(v)));
            }
            // 设置请求内容格式
            connection.setRequestProperty(CONTENT_TYPE_KEY,
                    contentType = XLPStringUtil.isEmpty(contentType) ? DEFAULT_CONTENT_TYPE : contentType);
        }

        private String encode(String v) throws UnsupportedEncodingException {
            return URLEncoder.encode(v, encodeCharset);
        }

        private String serializeParams(Map<?, ?> map) throws UnsupportedEncodingException {
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                Object key = entry.getKey();
                if (key != null){
                    sb.append("&").append(encode(String.valueOf(key))).append("=")
                            .append(encode(String.valueOf(entry.getValue())));
                }
            }
            return sb.toString();
        }

        /**
         * 创建{@link HttpRequestClientBuilder}对象
         * @return
         */
        public static HttpRequestClientBuilder create(){
            return new HttpRequestClientBuilder();
        }
    }

    /**
     * 请求数据格式函数
     */
    @FunctionalInterface
    public interface RequestDataFormatter{
        /**
         * 自定义格式化请求函数
         * @param data
         * @param contentType
         * @return
         */
        byte[] requestDataFormat(Object data, String contentType);
    }
}
